﻿// Alloy Physical Shader Framework
// Copyright 2013-2016 RUST LLC.
// http://www.alloy.rustltd.com/

using System;
using UnityEngine;
using UnityEngine.Rendering;
using MessagePack;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace PHIBL
{
    [ExecuteInEditMode]
    [ImageEffectAllowedInSceneView]
    [RequireComponent(typeof(Camera))]
    [AddComponentMenu(AlloyUtils.ComponentsPath + "Deferred Renderer Plus")]
    public class AlloyDeferredRendererPlus : MonoBehaviour
    {

        // Arbitrary range multiplier.
        private const float c_blurWdith = 0.15f;
        private const float c_blurDepthDifferenceMultiplier = 100.0f;
        private const string c_copyTransmissionBufferName = "AlloyCopyTransmission";
        private const string c_blurNormalsBufferName = "AlloyBlurNormals";
        private const CameraEvent c_copyTransmissionEvent = CameraEvent.AfterGBuffer;
        private const CameraEvent c_blurNormalsEvent = CameraEvent.BeforeLighting;
        [MessagePackObject(true)]
        [Serializable]
        public struct SkinSettingsData
        {
            public bool Enabled;
            [IgnoreMember]
            public Texture2D Lut;

            [Range(0.0f, 1.0f)]
            public float Weight;

            [Range(0.01f, 1.0f)]
            public float MaskCutoff;

            [Range(0.0f, 1.0f)]
            public float Bias;

            [Range(0.0f, 1.0f)]
            public float Scale;

            [Range(0.0f, 1.0f)]
            public float BumpBlur;

            public Vector3 Absorption;
            public Vector3 AoColorBleed;
        }
        [MessagePackObject(true)]
        [Serializable]
        public struct TransmissionSettingsData
        {
            public bool Enabled;

            [Range(0.0f, 1.0f)]
            public float Weight;

            [Range(0.0f, 1.0f)]
            public float ShadowWeight;

            [Range(0.0f, 1.0f)]
            [Tooltip("Amount that the transmission is distorted by surface normals.")]
            public float BumpDistortion;

            [MinValue(1.0f)]
            public float Falloff;
        }

        public SkinSettingsData SkinSettings = new SkinSettingsData()
        {
            Enabled = true,
            Weight = 1.0f,
            MaskCutoff = 0.1f,
            Bias = 0.5f,
            Scale = 1.0f,
            BumpBlur = 0.7f,
            Absorption = new Vector3(-8.0f, -40.0f, -64.0f),
            AoColorBleed = new Vector3(0.4f, 0.15f, 0.13f),
        };

        public TransmissionSettingsData TransmissionSettings = new TransmissionSettingsData
        {
            Enabled = true,
            Weight = 1.0f,
            ShadowWeight = 0.5f,
            BumpDistortion = 0.5f,
            Falloff = 5.0f
        };

        // LUT
        [HideInInspector] public Texture2D SkinLut;

        // Shaders
        [HideInInspector] public Shader DeferredTransmissionBlit;
        [HideInInspector] public Shader DeferredBlurredNormals;

        // Private
        private Material m_deferredTransmissionBlitMaterial;
        private Material m_deferredBlurredNormalsMaterial;

        private Camera m_camera;
        private bool m_isTransmissionEnabled;
        private bool m_isScatteringEnabled;

        private CommandBuffer m_copyTransmission;
        private CommandBuffer m_renderBlurredNormals;

#if UNITY_EDITOR
    private int lastWidth = 0;
    private int lastHeight = 0;
#endif

        private void Awake()
        {
            m_camera = GetComponent<Camera>();
        }

        public void Reset()
        {
            ResetCommandBuffers();
        }

#if UNITY_EDITOR
    private void Update() {
        if (lastWidth != m_camera.pixelWidth
            || lastHeight != m_camera.pixelHeight) {
            ResetCommandBuffers();
        }
    }
#endif

        private void OnEnable()
        {
            ResetCommandBuffers();
        }

        private void OnDisable()
        {
            DestroyCommandBuffers();
        }

        private void OnDestroy()
        {
            DestroyCommandBuffers();
        }

        public void Refresh()
        {
            bool scatteringEnabled = SkinSettings.Enabled;
            bool transmissionEnabled = TransmissionSettings.Enabled || scatteringEnabled;

            if (m_isTransmissionEnabled == transmissionEnabled
                && m_isScatteringEnabled == scatteringEnabled)
            {
                RefreshProperties();
            }
            else
            {
                ResetCommandBuffers();
            }
        }

        // Per camera properties.
        private void RefreshProperties()
        {
            if (m_isTransmissionEnabled || m_isScatteringEnabled)
            {
                float transmissionWeight = m_isTransmissionEnabled ? Mathf.GammaToLinearSpace(TransmissionSettings.Weight) : 0.0f;

                Shader.SetGlobalVector("_DeferredTransmissionParams",
                    new Vector4(transmissionWeight, TransmissionSettings.Falloff, TransmissionSettings.BumpDistortion, TransmissionSettings.ShadowWeight));

                if (m_isScatteringEnabled)
                {
                    // Blur shaders.
                    float distanceToProjectionWindow = 1.0f / Mathf.Tan(0.5f * Mathf.Deg2Rad * m_camera.fieldOfView);
                    float blurStepScale = c_blurWdith * distanceToProjectionWindow;
                    float blurDepthDifferenceScale = c_blurDepthDifferenceMultiplier * distanceToProjectionWindow;

                    Shader.SetGlobalVector("_DeferredBlurredNormalsParams", new Vector2(blurStepScale, blurDepthDifferenceScale));

                    // Material shaders.
                    var absorption = SkinSettings.Absorption;
                    var aoColorBleed = SkinSettings.AoColorBleed;

                    Shader.SetGlobalTexture("_DeferredSkinLut", SkinSettings.Lut);
                    Shader.SetGlobalVector("_DeferredSkinParams", new Vector3(SkinSettings.Weight, 1.0f / SkinSettings.MaskCutoff, SkinSettings.BumpBlur));
                    Shader.SetGlobalVector("_DeferredSkinTransmissionAbsorption", new Vector4(absorption.x, absorption.y, absorption.z, SkinSettings.Bias));
                    Shader.SetGlobalVector("_DeferredSkinColorBleedAoWeights", new Vector4(aoColorBleed.x, aoColorBleed.y, aoColorBleed.z, SkinSettings.Scale));
                }
            }
        }

        private void ResetCommandBuffers()
        {
            m_isScatteringEnabled = SkinSettings.Enabled;
            m_isTransmissionEnabled = TransmissionSettings.Enabled || m_isScatteringEnabled;

            if (SkinSettings.Lut == null)
            {
                SkinSettings.Lut = SkinLut;

#if UNITY_EDITOR
            EditorUtility.SetDirty(this);
#endif
            }

            DestroyCommandBuffers();

            if ((m_isTransmissionEnabled || m_isScatteringEnabled)
                && m_camera != null
                && DeferredTransmissionBlit != null)
            {
                int outputRT = Shader.PropertyToID("_DeferredPlusBuffer");

#if UNITY_EDITOR
            // Reference for when screen size changes.
            lastWidth = m_camera.pixelWidth;
            lastHeight = m_camera.pixelHeight;
#endif

                m_deferredTransmissionBlitMaterial = new Material(DeferredTransmissionBlit)
                {
                    hideFlags = HideFlags.HideAndDontSave
                };

                // Copy Gbuffer emission buffer so we can get at the alpha channel for transmission.
                m_copyTransmission = new CommandBuffer
                {
                    name = c_copyTransmissionBufferName
                };

                if (!m_isScatteringEnabled)
                {
                    // Copy transmission from emission buffer alpha.
                    m_copyTransmission.GetTemporaryRT(outputRT, -1, -1, 0, FilterMode.Point, RenderTextureFormat.ARGBHalf, RenderTextureReadWrite.Linear);
                    m_copyTransmission.Blit(BuiltinRenderTextureType.CameraTarget, outputRT, m_deferredTransmissionBlitMaterial);
                    m_copyTransmission.ReleaseTemporaryRT(outputRT);
                }
                else if (DeferredBlurredNormals != null)
                {
                    int halfWidth = m_camera.pixelWidth / 2;
                    int halfHeight = m_camera.pixelHeight / 2;
                    int pingRT = Shader.PropertyToID("_DeferredBlurredNormalPingBuffer");
                    int pongRT = Shader.PropertyToID("_DeferredBlurredNormalPongBuffer");
                    int _DeferredTransBuff = Shader.PropertyToID("_DeferredTransmissionBuffer");
                    // Bind emission buffer to be copied in blurred normal upsample pass.
                    m_copyTransmission.SetGlobalTexture(_DeferredTransBuff, BuiltinRenderTextureType.CameraTarget);

                    // Blur normals and copy transmission.
                    m_deferredBlurredNormalsMaterial = new Material(DeferredBlurredNormals)
                    {
                        hideFlags = HideFlags.HideAndDontSave
                    };

                    m_renderBlurredNormals = new CommandBuffer
                    {
                        name = c_blurNormalsBufferName
                    };

                    // RGBA8 target has sufficient precision for normals that are smooth and diffuse-only.
                    m_renderBlurredNormals.GetTemporaryRT(outputRT, -1, -1, 0, FilterMode.Point, RenderTextureFormat.ARGBHalf, RenderTextureReadWrite.Linear);
                    m_renderBlurredNormals.GetTemporaryRT(pingRT, halfWidth, halfHeight, 0, FilterMode.Point, RenderTextureFormat.ARGBHalf, RenderTextureReadWrite.Linear);
                    m_renderBlurredNormals.GetTemporaryRT(pongRT, halfWidth, halfHeight, 0, FilterMode.Point, RenderTextureFormat.ARGBHalf, RenderTextureReadWrite.Linear);

                    // Downsample, Blur X, Blur Y, Upsample.
                    m_renderBlurredNormals.Blit(BuiltinRenderTextureType.GBuffer2, pingRT, m_deferredBlurredNormalsMaterial, 0);
                    m_renderBlurredNormals.Blit(pingRT, pongRT, m_deferredBlurredNormalsMaterial, 1);
                    m_renderBlurredNormals.Blit(pongRT, pingRT, m_deferredBlurredNormalsMaterial, 2);
                    m_renderBlurredNormals.Blit(pingRT, outputRT, m_deferredBlurredNormalsMaterial, 3);

                    // Cleanup.
                    m_renderBlurredNormals.ReleaseTemporaryRT(outputRT);
                    m_renderBlurredNormals.ReleaseTemporaryRT(pingRT);
                    m_renderBlurredNormals.ReleaseTemporaryRT(pongRT);

                    // Need depth texture for depth-aware upsample.
                    m_camera.depthTextureMode |= DepthTextureMode.Depth;
                    m_camera.AddCommandBuffer(c_blurNormalsEvent, m_renderBlurredNormals);
                }

                m_camera.AddCommandBuffer(c_copyTransmissionEvent, m_copyTransmission);
            }

            RefreshProperties();

#if UNITY_EDITOR
        EditorUtility.SetDirty(m_camera);
#endif
        }

        private void DestroyCommandBuffers()
        {
            if (m_copyTransmission != null)
            {
                m_camera.RemoveCommandBuffer(c_copyTransmissionEvent, m_copyTransmission);
            }

            if (m_renderBlurredNormals != null)
            {
                m_camera.RemoveCommandBuffer(c_blurNormalsEvent, m_renderBlurredNormals);
            }

            if (m_deferredTransmissionBlitMaterial != null)
            {
                DestroyImmediate(m_deferredTransmissionBlitMaterial);
            }

            if (m_deferredBlurredNormalsMaterial != null)
            {
                DestroyImmediate(m_deferredBlurredNormalsMaterial);
            }

            m_copyTransmission = null;
            m_renderBlurredNormals = null;
            m_deferredTransmissionBlitMaterial = null;
            m_deferredBlurredNormalsMaterial = null;
        }
    }
}